﻿using EasyCaching.Core;
using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace TK.Common.Cache
{
    public class DefaultEasyCachingCache : IDistributedCache
    {
        private readonly IEasyCachingProvider _provider;

        public DefaultEasyCachingCache(IEasyCachingProvider provider)
        {
            _provider = provider;
        }

        public byte[] Get(string key)
        {
            ArgumentCheck.NotNull(key, nameof(key));
            return GetAndRefreshData(key);
        }

        public async Task<byte[]> GetAsync(string key, CancellationToken token = default)
        {
            ArgumentCheck.NotNull(key, nameof(key));
            token.ThrowIfCancellationRequested();
            return await Task.FromResult(GetAndRefreshData(key));
        }

        public void Refresh(string key)
        {
            ArgumentCheck.NotNull(key, nameof(key));
            GetAndRefreshData(key);
        }

        public async Task RefreshAsync(string key, CancellationToken token = default)
        {
            ArgumentCheck.NotNull(key, nameof(key));
            token.ThrowIfCancellationRequested();
            GetAndRefreshData(key);
            await Task.CompletedTask;
        }

        public void Remove(string key)
        {
            ArgumentCheck.NotNull(key, nameof(key));
            _provider.Remove(key);
        }

        public async Task RemoveAsync(string key, CancellationToken token = default)
        {
            ArgumentCheck.NotNull(key, nameof(key));
            await _provider.RemoveAsync(key);
        }

        public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
        {
            ArgumentCheck.NotNull(key, nameof(key));
            ArgumentCheck.NotNull(value, nameof(value));
            ArgumentCheck.NotNull(options, nameof(options));
            var tupleTime = GetExpiration(options);
            var newValue = GetExetendValue(value, tupleTime);
            _provider.Set(key, newValue, tupleTime.Item2);
        }

        public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
        {
            ArgumentCheck.NotNull(key, nameof(key));
            ArgumentCheck.NotNull(value, nameof(value));
            ArgumentCheck.NotNull(options, nameof(options));
            var tupleTime = GetExpiration(options);
            var newValue = GetExetendValue(value, tupleTime);
            await _provider.SetAsync(key, newValue, tupleTime.Item2) ;
        }

        #region private methods

        private byte[] GetAndRefreshData(string key)
        {
            var cacheValue = _provider.Get<byte[]>(key);
            if (cacheValue.Value != null)
            {
                var abMsBytes = new byte[8];
                var slMsBytes = new byte[8];
                var data = new byte[cacheValue.Value.Length - 16];
                Array.Copy(cacheValue.Value, cacheValue.Value.Length - 16, abMsBytes, 0, 8);
                Array.Copy(cacheValue.Value, cacheValue.Value.Length - 8, slMsBytes, 0, 8);
                Array.Copy(cacheValue.Value, 0, data, 0, data.Length);
                var slTime = TimeSpan.FromMilliseconds(BitConverter.ToInt64(slMsBytes));
                var abUtcTime = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(abMsBytes));
                var curUtcTime = DateTimeOffset.UtcNow;
                if (slTime > TimeSpan.Zero && abUtcTime > curUtcTime)
                {
                    slTime = abUtcTime - curUtcTime > slTime ? (abUtcTime - curUtcTime) : slTime;
                    _provider.Set(key, cacheValue.Value, slTime);
                }

                return data;
            }

            return cacheValue.Value;
        }

        private byte[] GetExetendValue(byte[] value, (long, TimeSpan, long) tupleTime)
        {
            var result = new List<byte>(value.Length + 16);
            result.AddRange(value);
            result.AddRange(BitConverter.GetBytes(tupleTime.Item1));
            result.AddRange(BitConverter.GetBytes(tupleTime.Item3));
            return result.ToArray();
        }

        private (long, TimeSpan, long) GetExpiration(DistributedCacheEntryOptions options)
        {
            var curUtcTime = DateTimeOffset.UtcNow;
            var time = TimeSpan.FromSeconds(1);
            var expireUtcTime = curUtcTime;
            var slideTime = options.SlidingExpiration.HasValue ? options.SlidingExpiration.Value : TimeSpan.Zero;
            if (options.AbsoluteExpiration.HasValue && options.AbsoluteExpiration.Value.ToUniversalTime() <= curUtcTime)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(DistributedCacheEntryOptions.AbsoluteExpiration),
                    options.AbsoluteExpiration,
                    "The absolute expiration value must be in the future.");
            }

            if (options.AbsoluteExpirationRelativeToNow.HasValue)
            {
                time = options.AbsoluteExpirationRelativeToNow.Value;
                expireUtcTime = curUtcTime.AddMilliseconds(time.TotalMilliseconds);
            }

            if (options.AbsoluteExpiration.HasValue && expireUtcTime < options.AbsoluteExpiration.Value)
            {
                expireUtcTime = options.AbsoluteExpiration.Value;
                time = options.AbsoluteExpiration.Value.ToUniversalTime() - curUtcTime;
            }

            return (expireUtcTime.ToUnixTimeMilliseconds(), time, (long)slideTime.TotalMilliseconds);
        }
    }
    #endregion
}
